﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
using SSG.Tools;

namespace SSG.Model
{
    /// <summary>
    /// Base for SID, STAR, Approach ...
    /// </summary>
    public abstract partial class Procedure : BaseEntity
    {
        private readonly List<Fix> _fixes = new List<Fix>();
        private readonly List<Runway> _runways = new List<Runway>();
        private readonly List<Transition> _transistions = new List<Transition>();
        private static readonly Regex RegExNumber = new Regex(@"\d+");
        public const string KmlTrackStyle = "Procedure";

        /// <summary>
        /// For name generation
        /// </summary>
        public enum SmartNameMode
        {
            FirstFixPlus2Char,
            LastFixPlus2Char,
            FirstFixPlusSplitAtNumeric,
            LastFixPlusSplitAtNumeric,
            Name
        }

        /// <summary>
        /// Smarter name based on type
        /// </summary>
        /// <see cref="GetSmartName(bool)"/>
        [XmlIgnore]
        public string SmartName { get { return this.GetSmartName(); } }

        /// <summary>
        /// Smarter name based on type
        /// </summary>
        /// <see cref="GetSmartName(bool)"/>
        [XmlIgnore]
        public string SmartNameNoSpaces { get { return this.GetSmartName(false); } }

        /// <summary>
        /// No space in name, replaced by "_"
        /// </summary>
        [XmlIgnore]
        public string SmartNameSpaceToUnderscore
        {
            get
            {
                string sn = this.SmartName;
                return String.IsNullOrEmpty(sn) ? sn : sn.Replace(' ', '_');
            }
        }

        /// <summary>
        /// Fixes
        /// </summary>
        public List<Fix> Fixes { get { return this._fixes; } }

        /// <summary>
        /// Max.altitude
        /// </summary>
        public Altitude MaxAltitude { get { return this.Fixes.MaxAltitude(); } }

        /// <summary>
        /// Min.altitude
        /// </summary>
        public Altitude MinAltitude { get { return this.Fixes.MinAltitude(); } }

        /// <summary>
        /// Fixes
        /// </summary>
        public List<Fix> TransitionsFixes
        {
            get
            {
                List<Fix> fixes = new List<Fix>();
                if (this._transistions.NullSafeIsEmpty()) return fixes;
                foreach (Transition tr in this._transistions) {
                    fixes.AddRange(tr.Fixes);
                }
                return fixes;
            }
        }

        /// <summary>
        /// Max.altitude of transitions
        /// </summary>
        public Altitude TransitionsMaxAltitude { get { return this.TransitionsFixes.MaxAltitude(); } }

        /// <summary>
        /// Min.altitude of transitions
        /// </summary>
        public Altitude TransitionsMinAltitude { get { return this.TransitionsFixes.MinAltitude(); } }

        /// <summary>
        /// Corresponding runways
        /// </summary>
        public List<Runway> Runways { get { return this._runways; } }

        /// <summary>
        /// Corresponding runway tracks
        /// </summary>
        public List<Transition> Transitions { get { return this._transistions; } }

        /// <summary>
        /// Corresponding airport
        /// </summary>
        public Airport Airport { get; set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="name"></param>
        /// <param name="airport"></param>
        /// <param name="runway"></param>
        protected Procedure(string name, Airport airport, Runway runway) : base(name, null, null)
        {
            if (String.IsNullOrEmpty(name)) throw new ArgumentNullException("name");
            this.Airport = airport;
            if (runway != null) this.Runways.Add(runway);
        }

        /// <summary>
        /// Valid for given runway designator
        /// </summary>
        /// <param name="runway"></param>
        /// <returns></returns>
        public bool IsValidForRunway(string runway)
        {
            if (String.IsNullOrEmpty(runway) || this.Runways.NullSafeIsEmpty()) return false;
            return this._runways.Any(rw => rw.Name == runway || rw.CanonicalName == runway);
        }

        /// <summary>
        /// Add a new fixpoint.
        /// </summary>
        /// <param name="newFix"></param>
        public void AddFix(Fix newFix)
        {
            if (newFix == null) throw new ArgumentNullException("newFix");
            this._fixes.Add(newFix);
        }

        /// <summary>
        /// Add runway, if not already "linked"
        /// </summary>
        /// <param name="runway"></param>
        public void AddRunway(Runway runway)
        {
            if (runway == null) throw new ArgumentNullException("runway");
            if (this.Runways.Any(rw => rw.Name.Equals(runway.Name, StringComparison.InvariantCultureIgnoreCase))) return;
            this._runways.Add(runway);
        }

        /// <summary>
        /// Add runways
        /// </summary>
        /// <param name="runways"></param>
        public void AddRunways(List<Runway> runways)
        {
            if (runways.NullSafeIsEmpty()) return;
            foreach (var runway in runways) {
                this.AddRunway(runway);
            }
        }

        /// <summary>
        /// Add a transition.
        /// </summary>
        /// <param name="transition"></param>
        public void AddTransition(Transition transition)
        {
            if (transition == null) throw new ArgumentNullException("transition");
            this._transistions.Add(transition);
            transition.Description = this.Name + BaseEntity.CanonicalNameSeperator + transition.Name;
        }

        /// <summary>
        /// Navaids
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public List<Navaid> GetNavaids(Navaid.NavaidType type = Navaid.NavaidType.Any)
        {
            return this.Fixes.OfType<Navaid>().Where(navaid => type == Navaid.NavaidType.Any || navaid.Type == type).ToList();
        }

        /// <summary>
        /// Number of navaids
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public int CountNavaids(Navaid.NavaidType type = Navaid.NavaidType.Any)
        {
            return this.Fixes.OfType<Navaid>().Count(navaid => type == Navaid.NavaidType.Any || navaid.Type == type);
        }

        /// <summary>
        /// Any fixpoint in blacklist?
        /// </summary>
        /// <param name="settings"></param>
        /// <returns></returns>
        public bool InBlacklist(GeneratorSettings settings)
        {
            if (settings == null || settings.FixBlacklist == null) return false;
            return !this.Fixes.NullSafeIsEmpty() && this.Fixes.Any(f => f.InBlacklist(settings));
        }

        /// <summary>
        /// Is and fixpoint in blacklist?
        /// </summary>
        /// <param name="settings"></param>
        /// <returns></returns>
        public List<Fix> FixesInBlacklist(GeneratorSettings settings)
        {
            if (settings == null || settings.FixBlacklist == null) return new List<Fix>();
            return this.Fixes.Where(fix => fix.InBlacklist(settings)).ToList();
        }

        /// <summary>
        /// Name string of fixes blacklisted
        /// </summary>
        /// <param name="settings"></param>
        /// <returns></returns>
        public String FixNamesInBlacklist(GeneratorSettings settings)
        {
            List<Fix> fixes = this.FixesInBlacklist(settings);
            return fixes.NullSafeIsEmpty() ? "" : BaseEntity.Names(fixes).EnumerationToString();
        }

        /// <summary>
        /// Fixes, blacklist considered. With runway passed, fixes are specific to one runway, e.g.
        /// by supporting runway tracks. Otherwise only the direct fixes are considered.
        /// </summary>
        /// <param name="settings"></param>
        /// <param name="blacklistedFixes"></param>
        /// <param name="runway"></param>
        /// <returns></returns>
        public List<Fix> FixesBlacklistConsidered(GeneratorSettings settings, out List<Fix> blacklistedFixes, Runway runway = null)
        {
            if (settings == null) settings = GeneratorSettings.DefaultSettings;
            if (settings == null) throw new InvalidOperationException("No settings");
            blacklistedFixes = new List<Fix>();
            if (!settings.IsBlacklistEnabled || settings.FixBlacklist.NullSafeIsEmpty()) return this.FixesWithRunwayTracks(runway, settings);
            List<Fix> notInBlacklist = new List<Fix>();
            List<Fix> fixes = (runway == null) ? this.Fixes : this.FixesWithRunwayTracks(runway, settings);
            foreach (Fix fix in fixes) {
                if (fix.InBlacklist(settings))
                    blacklistedFixes.Add(fix);
                else {
                    notInBlacklist.Add(fix);
                }
            }
            return notInBlacklist;
        }

        /// <summary>
        /// Link with multiple runways.
        /// </summary>
        /// <param name="content"></param>
        /// <param name="airportRunways"></param>
        internal void AddRunwaysFromPmdgFormat(string content, List<Runway> airportRunways)
        {
            if (String.IsNullOrEmpty(content)) return;
            content = content.Trim().ToUpperInvariant();
            if (!content.StartsWith("RNW")) return;
            string[] parts = content.Split(' ');
            foreach (string part in parts) {
                if ("RNW".Equals(part, StringComparison.InvariantCultureIgnoreCase)) continue;
                // Add by reference! Important for search ...
                Runway rw = BaseEntity.FindByName(part, airportRunways);
                if (rw == null) throw new InvalidOperationException("No Runway " + part + " for " + this);
                this.AddRunway(rw);
            }
        }

        /// <summary>
        /// From Navigraph line.
        /// </summary>
        /// <param name="cl"></param>
        /// <param name="airport"></param>
        /// <param name="runway"></param>
        /// <returns></returns>
        public static Procedure FromPmdgFormat(string cl, Airport airport, Runway runway = null)
        {
            // checks
            if (String.IsNullOrWhiteSpace(cl)) throw new ArgumentNullException("cl");
            if (airport == null) throw new ArgumentNullException("airport");

            cl = cl.Trim();
            string[] parts = cl.Split(' ');
            if (parts.Length < 2) throw new FmcParseException("Line " + cl + " for airport cannot be parsed") {Content = cl};
            string raw = parts.Length < 3 ? null : cl.Substring(cl.IndexOf(parts[2], System.StringComparison.InvariantCultureIgnoreCase));
            Procedure track;
            if ("SID".Equals(parts[0], StringComparison.InvariantCultureIgnoreCase))
                track = new Sid(parts[1], airport, runway) {DataRawString = raw};
            else if ("STAR".Equals(parts[0], StringComparison.InvariantCultureIgnoreCase))
                track = new Star(parts[1], airport, runway) {DataRawString = raw};
            else if ("APPROACH".Equals(parts[0], StringComparison.InvariantCultureIgnoreCase))
                track = new Approach(parts[1], airport, runway) {DataRawString = raw};
            else if ("TRANSITION".Equals(parts[0], StringComparison.InvariantCultureIgnoreCase))
                track = new Transition(parts[1], airport, runway) {DataRawString = raw};
            else if ("RNW".Equals(parts[0], StringComparison.InvariantCultureIgnoreCase))
                track = new RunwayTrack(parts[1], airport, runway) {DataRawString = raw};
            else
                throw new FmcParseException("Unknown type " + parts[0]) {Content = cl};

            // add fixes if provided
            // the added fixes will be specific (Altitude, DataRawString) to the Procedure
            // TODO: Write advanced parser handling TRK, Headings etc such as "SID UNKE1J RNW 08 TRK 079 UNTIL 4.0 FROM FIX_ RW08 FIX_ ABNB FIX_ D0OEJ AT OR ABOVE 4800 TRK 065 UNTIL 7.0 FROM FIX_ D0OEJ TRK 065 UNTIL 9500 TURN LEFT DIRECT FIX_ RTTNB FIX_ UNKEN"
            // SSG: Parsing in procedure, central functionality for parsing SIDs, STARs, ..
            if (!airport.Fixes.NullSafeIsEmpty()) {
                for (int p = 3; parts.Length > p; p++) {
                    string part = parts[p];
                    // Confirm this value belongs to a fixpoint
                    if (parts[p - 1] != "FIX" && parts[p - 2] != "FIX") continue; // Fixpoint ? The -2 => FIX_OVERFLY_XY
                    if (parts[p - 2] == "FROM") continue; // ignore "FROM_FIX_XYZ"
                    // since airport fixes are already converted to navaids if applicable
                    // correct navaids will be returned if any
                    Fix f = BaseEntity.FindByName(part, airport.Fixes);
                    if (f == null) continue;

                    // clone the fixpoint, since Rawstring etc are specific to the Procedure
                    f = f.Clone() as Fix;
                    if (f == null) throw new InvalidOperationException("Failed cloning of fix");
                    f.DataRawString = Procedure.FixDetails(parts, p);
                    f.AltitudeRestriction = Altitude.AltitudeFromNavigraphPmdgFormat(f.DataRawString);
                    track.AddFix(f);
                } // for
            }

            // link with corresponding runways
            List<Runway> rws = airport.GetRunways(Runway.RunwayNamesFromPmdgLine(cl));
            if (!rws.NullSafeIsEmpty()) track.AddRunways(rws);

            // return
            return track;
        }

        /// <summary>
        /// Get the fixpoint detail parts.
        /// </summary>
        /// <param name="parts"></param>
        /// <param name="startIndex"></param>
        /// <returns></returns>
        private static string FixDetails(string[] parts, int startIndex)
        {
            if (parts.NullSafeIsEmpty()) return null;
            if (startIndex >= parts.Length) return null;
            StringBuilder sb = new StringBuilder(parts[startIndex]);
            for (int p = startIndex + 1; p < parts.Length; p++) {
                if ("FIX".Equals(parts[p])) {
                    if (!"FROM".Equals(parts[p - 1])) break;
                }
                else {
                    if ("HOLD".Equals(parts[p])) break;
                }
                sb.Append(' ');
                sb.Append(parts[p]);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Add a comment about blacklisted fixes
        /// </summary>
        /// <param name="blacklistedFixes"></param>
        /// <param name="sb"></param>
        private static void AddBlacklistedFixesComments(List<Fix> blacklistedFixes, StringBuilder sb)
        {
            if (sb == null) throw new ArgumentNullException("sb");
            if (blacklistedFixes.NullSafeIsEmpty()) return;
            sb.AppendIfNotNullOrEmpty("blacklist: ", Generator.SctEseCommentSeperator);
            sb.Append(BaseEntity.Names(blacklistedFixes).EnumerationToString());
        }

        /// <summary>
        /// Meaningful representation.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            if (String.IsNullOrEmpty(this.Name)) return base.ToString();
            return this.GetType().GetClassname() + " " + this.Name + " Rwys: " + this._runways.Count + " Trans: " + this._transistions.Count + " Fix: " + this.Fixes.Count;
        }

        /// <summary>
        /// Basically a debug info about the included navaids
        /// </summary>
        /// <returns></returns>
        public string NavaidCountString()
        {
            StringBuilder sb = new StringBuilder();
            foreach (Navaid.NavaidType type in Enum.GetValues(typeof (Navaid.NavaidType))) {
                if (type == Navaid.NavaidType.Any) continue;
                int count = this.CountNavaids(type);
                if (count < 1) continue;
                if (sb.Length > 0) sb.Append(' ');
                sb.Append(type.ToString().ToUpperInvariant());
                sb.Append(": ");
                sb.Append(count);
            }
            return sb.ToString();
        }

        /// <summary>
        /// Append debug info
        /// </summary>
        private string NavaidsInformation()
        {
            StringBuilder sb = new StringBuilder();
            List<Navaid> navaids = this.GetNavaids();
            if (navaids.NullSafeIsEmpty()) return "";
            sb.Append("Navaids repl. fixes: ");
            sb.Append(this.NavaidCountString());
            sb.Append(" / ");
            sb.Append(BaseEntity.GetNamesString(navaids));
            return sb.ToString();
        }

        /// <summary>
        /// Generating a smart name
        /// </summary>
        /// <param name="withSpace"></param>
        /// <returns></returns>
        private string GetSmartName(bool withSpace = true)
        {
            if (this.GetType() == typeof (Star)) return this.GetSmartName(SmartNameMode.FirstFixPlus2Char, withSpace);
            if (this.GetType() == typeof (Sid)) return this.GetSmartName(SmartNameMode.LastFixPlus2Char, withSpace);
            if (this.GetType() == typeof (Transition)) return this.GetSmartName(SmartNameMode.FirstFixPlusSplitAtNumeric, withSpace);
            return withSpace ? this.Name : this.Name.Replace(" ", String.Empty);
        }

        /// <summary>
        /// Generating a smart name
        /// </summary>
        /// <param name="mode"></param>
        /// <param name="withSpace"></param>
        /// <returns></returns>
        public string GetSmartName(SmartNameMode mode, bool withSpace = true)
        {
            if (String.IsNullOrEmpty(this.Name)) throw new InvalidOperationException("Missing name");
            String n = this.Name;
            String s = withSpace ? " " : "";
            String l2C = this.Name.NullSafeLastChars(2);
            String np = this.GetNumericNamePart();
            switch (mode) {
                case SmartNameMode.FirstFixPlus2Char:
                    if (!this.Fixes.NullSafeIsEmpty() && !String.IsNullOrEmpty(l2C)) n = this.Fixes.First().Name + s + l2C;
                    break;
                case SmartNameMode.LastFixPlus2Char:
                    if (!this.Fixes.NullSafeIsEmpty() && !String.IsNullOrEmpty(l2C)) n = this.Fixes.Last().Name + s + l2C;
                    break;
                case SmartNameMode.FirstFixPlusSplitAtNumeric:
                    if (!this.Fixes.NullSafeIsEmpty() && !String.IsNullOrEmpty(np)) n = this.Fixes.First().Name + s + np;
                    break;
                case SmartNameMode.LastFixPlusSplitAtNumeric:
                    if (!this.Fixes.NullSafeIsEmpty() && !String.IsNullOrEmpty(np)) n = this.Fixes.Last().Name + s + np;
                    break;
                case SmartNameMode.Name:
                    break;
                default:
                    throw new InvalidOperationException("Wrong smart mode");
            }
            return n.Trim(); // trim away spaces at the end, e.g. because numeric part was empty
        }

        /// <summary>
        /// Part starting with number
        /// </summary>
        /// <returns></returns>
        private String GetNumericNamePart()
        {
            if (String.IsNullOrEmpty(this.Name)) throw new InvalidOperationException("Missing name");
            Match m = Procedure.RegExNumber.Match(this.Name);
            return !m.Success ? "" : this.Name.Substring(m.Index);
        }

        /// <summary>
        /// Fixes with runway tracks
        /// </summary>
        /// <param name="runway"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public virtual List<Fix> FixesWithRunwayTracks(string runway, GeneratorSettings settings)
        {
            return this.Fixes;
        }

        /// <summary>
        /// Fixes with runway tracks
        /// </summary>
        /// <param name="runway"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public virtual List<Fix> FixesWithRunwayTracks(Runway runway, GeneratorSettings settings)
        {
            return this.Fixes;
        }
    }
}